﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Collections;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.IO;
using System.Threading;
using System.Media;
using OpenTK.Graphics;
using OpenTK.Platform;
using System.Text.RegularExpressions;

namespace Chip8
{
    public partial class frmEmu : Form
    {
        #region Variables
        public EventWaitHandle m_DrawWait = new EventWaitHandle(false, EventResetMode.AutoReset); //EventWaitHandle to halt CPU, and allow open GL to fully draw every frame.

        //[Memory, Registers, Flags, and Timers]
        public byte[] Memory = new byte[4096]; //System Memory
        public byte[] ResetMemory = new byte[4096]; //Reset Array used to copy to system memory when a game is reset.

        public ushort PC; //Program Counter
        public ushort AddressRegister; //Index Register
        public byte[] Vreg = new byte[16]; //15 8bit regests V0-VE, VF is used as a flag
        public ushort Opcode; //Current Opcode

        public Stack Stack; //Stack

        Random r = new Random(); //Random used in 0xC000 RND instruction

        public byte[] FrameBuffer = new byte[64 * 32]; //Graphics Array 1 = on, 0 = off, 2048 pixels total;

        public byte LastKeyPressed;   //Keypad - Stores Last Key Pressed

        public byte Delay_Timer; //Delay Timer - Counts down to 0 at 60hz, if 0 then game then sets it until the next time tested for 0.
        public byte Sound_Timer; //Sound Timer - Counts down to 0 at 60hz, if 0 we play a beep. The game then sets it until the next time tested for 0.

        public bool StopEmulation = true; //Stop emulation flag.
        public bool PauseEmulation; //Pause emulation flag.

        public byte[] regRPL = new byte[8];  //Fake RPL register

        public Thread EmuThread; //Emulation Thread

        public String RomName; //Name of Loaded Rom

        public struct Instruction //Instruction Breakdown
        {
            public ushort raw;
            public ushort opnum;
            public ushort nnn;
            public byte n;
            public byte x;
            public byte y;
            public byte kk;
        }

        frmDebugger DB = new frmDebugger();

        public Instruction instr; //General Declaration of our instruction breakdown.

        //OpenTK
        private Control m_ControlContext;
        private GraphicsContext m_GContext;
        private IWindowInfo m_WindowInfo;
        #endregion

        public void InitializeSystem()
        {
            Array.Clear(Memory, 0, Memory.Length);
            PC = 512; //Rom begins at 0x200
            AddressRegister = new ushort();
            Array.Clear(Vreg, 0, Vreg.Length); //15 8bit regests V0-VE
            Opcode = new ushort();
            Array.Clear(FrameBuffer, 0, FrameBuffer.Length);
            Array.Clear(regRPL, 0, regRPL.Length);
            Delay_Timer = new byte();
            Sound_Timer = new byte();
            StopEmulation = false;
            PauseEmulation = false;
            Stack = new Stack();
            Random r = new Random();
            pScreen.BackColor = Color.Black;
            LoadFonts();
        }

        public void InitOpenTK()
        {
            //Initialize OpenTK/GL control/window
            m_ControlContext = Control.FromHandle(pScreen.Handle);
            m_ControlContext.Paint += new PaintEventHandler(m_ControlContext_Paint);
            m_WindowInfo = Utilities.CreateWindowsWindowInfo(pScreen.Handle);
            m_GContext = new GraphicsContext(GraphicsMode.Default, m_WindowInfo);
            m_GContext.MakeCurrent(m_WindowInfo);
            m_GContext.LoadAll();
        }

        public frmEmu()
        {
            InitializeComponent();
        }

        public void StartEmulation()
        {
          while (!StopEmulation)
              {
              while (PauseEmulation)
              {
                  //pause
              }
                  //Emulate One Cycle 
                  EmulateCycle();

              }
            //If we close the rom lets reset everything
          InitializeSystem();
        }
    
        public void DecodeInst(ushort Instruction)
        {
            instr.raw = Instruction;
            instr.opnum = (ushort)((Instruction & 0xF000) >> 12);
            instr.nnn = (ushort)(Instruction & 0x0FFF);
            instr.n = (byte)(Instruction & 0x000F);
            instr.x = (byte)((Instruction & 0x0F00) >> 8);
            instr.y = (byte)((Instruction & 0x00F0) >> 4);
            instr.kk = (byte)(Instruction & 0x00FF);
        }
        

        public ushort GetOpcodeMask(ushort instruction)
        {
            ushort mask = 0xF000;
            byte opcode = (byte)((instruction & 0xF000) >> 12);
           
            switch (opcode)
            {
                case 0x0:
                    {
                        if (instruction > 0)
                        {
                            if ((instruction & 0xFFF0) == 0x00C0)
                            {
                                mask = 0xFFF0;
                            }
                            else
                            {
                                mask = 0xFFFF;
                            }
                        }
                        break;
                    }
                case 0xF:
                case 0xE: mask = 0xF0FF; break;
                case 0x8: mask = 0xF00F; break;
                default: mask = 0xF000; break;
            }

            return mask;
        }
        public byte ReadMemoryByte(ushort address)
        {
            return Memory[address];
        }

        public void WriteMemoryByte(ushort address, byte value)
            {
	            Memory[address] = value;
            }
       
        public void EmulateCycle()
        {
        
            //DB.MovetoInstruction(PC.ToString("X4"));
                //Merge Instructions
                Opcode = (ushort)(Memory[PC++] << 8 | Memory[PC++]);

                //Decode Opcode into Inst Struct
                DecodeInst(Opcode);
                
                //Log Instructions
                DB.BuildInstructionList((ushort)(PC - 2),this, Memory[PC - 2], Memory[PC - 1]);

                switch (instr.raw & GetOpcodeMask(instr.raw))
                {
                    case 0x00E0: //CLS - Clear the display.
                        //PictureBox1.Image = null;
                        Array.Clear(FrameBuffer, 0, FrameBuffer.Length);
                        m_ControlContext.Invalidate();
                        m_DrawWait.WaitOne();
                        break;
                    case 0x00EE: //RET - Return from a subroutine. The interpreter sets the program counter to the address at the top of the stack, then subtracts 1 from the stack pointer.
                        if (Stack.Count > 0)
                            PC = (ushort)Stack.Pop();
                        else
                            PC = 512;
                        break;
                    case 0x1000: //JP - Jump to location nnn. The interpreter sets the program counter to nnn.
                        PC = instr.nnn;
                        break;
                    case 0x2000: //JSR - Push current PC to stack, and set PC = nnn.         
                        Stack.Push(PC);
                        PC = instr.nnn;
                        break;

                    case 0x3000: //SE - Skip next instruction if Vx = kk. The interpreter compares register Vx to kk, and if they are equal, increments the program counter by 2.
                        if (Vreg[instr.x] == instr.kk)
                            PC += 2;

                        break;
                    case 0x4000: //SNE - Skip next instruction if Vx != kk. The interpreter compares register Vx to kk, and if they are not equal, increments the program counter by 2. 
                        if (Vreg[instr.x] != instr.kk)
                            PC += 2;

                        break;
                    case 0x5000: //SE - Skip next instruction if Vx = Vy. The interpreter compares register Vx to register Vy, and if they are equal, increments the program counter by 2.
                        if (Vreg[instr.x] == Vreg[instr.y])
                            PC += 2;

                        break;
                    case 0x6000: //LD Set Vx = kk. The interpreter puts the value kk into register Vx.
                        Vreg[instr.x] = (byte)instr.kk;
                        break;
                    case 0x7000: //ADD Set Vx = Vx + kk. Adds the value kk to the value of register Vx, then stores the result in Vx.
                        Vreg[instr.x] += (byte)instr.kk;
                        break;
                    case 0x8000: //LD - Set Vx = Vy. Stores the value of register Vy in register Vx.
                        Vreg[instr.x] = Vreg[instr.y];
                        break;
                    case 0x8001: //OR -Set Vx = Vx OR Vy.
                        Vreg[instr.x] |= Vreg[instr.y];
                        Vreg[0xF] = 0;
                        break;
                    case 0x8002: //AND - Set Vx = Vx AND Vy.
                        Vreg[instr.x] &= Vreg[instr.y];
                        Vreg[0xF] = 0;
                        break;
                    case 0x8003: //XOR - Set Vx = Vx XOR Vy.
                        Vreg[instr.x] ^= Vreg[instr.y];
                        Vreg[0xF] = 0;
                        break;
                    case 0x8004: //ADD - Set Vx = Vx + Vy, set VF = carry.
                        int result = Vreg[instr.x] + Vreg[instr.y];
                        if (result > 0xFF)
                            Vreg[0xF] = 1;
                        else
                            Vreg[0xF] = 0;

                        Vreg[instr.x] = (byte)(result);

                        break;
                    case 0x8005: //SUB - Set Vx = Vx - Vy, set VF = NOT borrow.
                        if (Vreg[instr.x] >= Vreg[instr.y])
                            Vreg[0xF] = 1;
                        else
                            Vreg[0xF] = 0;

                        Vreg[instr.x] -= Vreg[instr.y];
                        break;
                    case 0x8006: //SHR - Set Vx = Vx SHR 1.
                        if ((Vreg[instr.x] & 1) == 1)
                            Vreg[0xF] = 1;
                        else
                            Vreg[0xF] = 0;

                        Vreg[instr.x] /= 2;
                        break;
                    case 0x8007: //SUBN - Set Vx = Vy - Vx, set VF = NOT borrow.
                        if (Vreg[instr.y] >= Vreg[instr.x])
                            Vreg[0xF] = 1;
                        else
                            Vreg[0xF] = 0;

                        Vreg[instr.x] = (byte)((Vreg[instr.y] - Vreg[instr.x]));

                        break;
                    case 0x800E: //SHL - Set Vx = Vx SHL 1.
                        if (((Vreg[instr.x] & 0x80) >> 7) == 1)
                            Vreg[0xF] = 1;
                        else
                            Vreg[0xF] = 0;

                        Vreg[instr.x] *= 2;

                        break;
                    case 0x9000: //SNE - Skip next instruction if Vx != Vy.
                        if (Vreg[instr.x] != Vreg[instr.y])
                            PC += 2;

                        break;
                    case 0xA000: //LD - Set I = nnn.
                        AddressRegister = instr.nnn;
                        break;
                    case 0xB000: //JMP - Jump to location nnn + V0.
                        PC = (ushort)(instr.nnn + Vreg[0]);
                        break;
                    case 0xC000: //RND - Set Vx = random byte AND kk.
                        Vreg[instr.x] = (byte)(r.Next(255) & instr.kk);
                        break;
                    case 0xD000: //DRW - Draw Screen.
                        ushort x = Vreg[instr.x];
                        ushort y = Vreg[instr.y];
                        ushort height = (ushort)instr.n;
                        ushort pixel;

                        Vreg[0xF] = 0;
                        for (int yline = 0; yline < height; yline++)
                        {
                            pixel = Memory[AddressRegister + yline];
                            for (int xline = 0; xline < 8; xline++)
                            {
                                if ((pixel & (0x80 >> xline)) != 0)
                                {
                                    if (FrameBuffer[(x + xline + ((y + yline) * 64))] == 1)
                                        Vreg[0xF] = 1;

                                    FrameBuffer[x + xline + ((y + yline) * 64)] ^= 1;
                                }
                            }
                        }
                      
                        m_ControlContext.Invalidate();
                        m_DrawWait.WaitOne();
                        //DrawGDI(); //Update the screen
                        break;
                    case 0xE09E: //SKP - Skip next instruction if key with the value of Vx is pressed.
                        if (Vreg[instr.x] == LastKeyPressed)
                        {
                            LastKeyPressed = 0;
                            PC += 2;
                        }

                        break;
                    case 0xE0A1: //SKNP - Skip next instruction if key with the value of Vx is not pressed.
                        if (Vreg[instr.x] != LastKeyPressed)
                        {
                            LastKeyPressed = 0;
                            PC += 2;
                        }

                        break;
                    case 0xF007: //LD - Set Vx = delay timer value.
                        if (Delay_Timer < 0)
                            Delay_Timer = 0;

                        Vreg[instr.x] = (byte)(Delay_Timer);
                        break;
                    case 0xF00A: //LD - Wait for a key press, store the value of the key in Vx.
                        while (LastKeyPressed == 0)
                        {
                            //Wait for Input
                        }
                        Vreg[instr.x] = LastKeyPressed;
                        LastKeyPressed = 0;
                        break;
                    case 0xF015: //LD - Set delay timer = Vx.
                        Delay_Timer = Vreg[instr.x];
                        break;
                    case 0xF018: //LD - Set sound timer = Vx.
                        Sound_Timer = Vreg[instr.y];
                        break;
                    case 0xF01E: //ADD - Set I = I + Vx.
                        if ((AddressRegister + Vreg[instr.x]) >= 0x1000)
                        {
                            AddressRegister = (ushort)(Memory.Length + 512);
                            Vreg[0xF] = 1;
                        }
                        else
                            AddressRegister += Vreg[instr.x];

                        break;
                    case 0xF029: //LD - Set I = location of sprite for digit Vx.
                        AddressRegister = (ushort)(Vreg[instr.x] * 5);
                        break;
                    case 0xF033: //LD - Store BCD representation of Vx in memory locations I, I+1, and I+2.
                        byte val = Vreg[instr.x];
                        WriteMemoryByte((ushort)(AddressRegister), (byte)(val / 100));
                        WriteMemoryByte((ushort)(AddressRegister + 1), (byte)((val % 100) / 10));
                        WriteMemoryByte((ushort)(AddressRegister + 2), (byte)((val % 100) % 10));
                        break;
                    case 0xF055: //LD - Store registers V0 through Vx in memory starting at location I.
                        for (var i = 0; i <= instr.x; i++)
                            WriteMemoryByte((ushort)(AddressRegister + i), (byte)(Vreg[i]));

                        break;
                    case 0xF065: //LD - Read registers V0 through Vx from memory starting at location I.
                        for (var i = 0; i <= instr.x; i++)
                            Vreg[i] = ReadMemoryByte((ushort)(AddressRegister + i));

                        break;
                    case 0xF075:
                        for (var i = 0; i <= instr.x; i++)
                            regRPL[i] = Vreg[i];

                        break;
                    case 0xF085:
                        for (var i = 0; i <= instr.x; i++)
                            Vreg[i] = regRPL[i];

                        break;
                    default:
                        break;
                }

     
                     
        }

        private void LoadFonts()
        {
            byte[] Temp = new Byte[0];

            using (FileStream fsSource = new FileStream(Path.GetDirectoryName(Application.ExecutablePath) + "\\c8fnt .bin", FileMode.Open, FileAccess.Read))
                {
                    Temp = new byte[fsSource.Length];
                    int numBytesToRead = (int)fsSource.Length;
                    int numBytesRead = 0;
                    while (numBytesToRead > 0)
                    {
                        int n = fsSource.Read(Temp, numBytesRead, numBytesToRead);
                        if (n == 0)
                            break;

                        numBytesRead += n;
                        numBytesToRead -= n;
                    }
                    numBytesToRead = Temp.Length;
                }

                //Load rom into memory from temp array
                for (int x = 0; x < Temp.Length; x++)
                    Memory[x] = Temp[x];

            
        }

        public void ResetGame()
        {
            if (StopEmulation == false)
            {
                StopEmulation = true;
                if (EmuThread != null)
                {
                    EmuThread.Abort();
                    while (EmuThread.ThreadState == ThreadState.Running)
                    {
                        //Wait Till previous thread exits
                    }
                }
                Array.Clear(FrameBuffer, 0, FrameBuffer.Length);
                Array.Clear(Vreg, 0, Vreg.Length);
                AddressRegister = 0;
                StopEmulation = false;
                PC = 512;
                Array.Copy(ResetMemory, Memory, 4096);
                PauseEmulation = false;
                EmuThread = new Thread(new ThreadStart(StartEmulation));
                EmuThread.IsBackground = true;
                EmuThread.Start();
                timer1.Enabled = true;
            }
        }

        public void CloseRom()
        {
            timer1.Enabled = false;
            StopEmulation = true;
            if (EmuThread != null)
            {
                EmuThread.Abort();
                while (EmuThread.ThreadState == ThreadState.Running)
                {
                    //Wait Till previous thread exits
                }
            }
            Array.Clear(FrameBuffer, 0, FrameBuffer.Length);
            Array.Clear(Vreg, 0, Vreg.Length);
            AddressRegister = 0;
            PC = 512;
            m_ControlContext.Invalidate();
            toolStripStatusLabel1.Text = "Emulation Stopped";
        }
        private void loadRomToolStripMenuItem_Click(object sender, EventArgs e)
        {
            StopEmulation = true; //Stop any previous emulation
            //Read file into temp array
            byte[] Temp = new Byte[0];
            OpenFileDialog fd = new OpenFileDialog();
            if (fd.ShowDialog() == DialogResult.OK)
            {
                CloseRom();

                    RomName = fd.FileName.Substring(fd.FileName.LastIndexOf("\\") + 1);

                using (FileStream fsSource = new FileStream(fd.FileName, FileMode.Open, FileAccess.Read))
                {
                    Temp = new byte[fsSource.Length];
                    int numBytesToRead = (int)fsSource.Length;
                    int numBytesRead = 0;
                    while (numBytesToRead > 0)
                    {
                        int n = fsSource.Read(Temp, numBytesRead, numBytesToRead);
                        if (n == 0)
                            break;

                        numBytesRead += n;
                        numBytesToRead -= n;
                    }
                    numBytesToRead = Temp.Length;
                }
                
                //Set Memory, Flags, Registers, etc... to default values
                InitializeSystem();

                //Load rom into memory from temp array
                for (int x = 0; x < Temp.Length; x++)
                    Memory[x + 512] = Temp[x];

                for (int x = 0; x < Temp.Length; x += 2)
                    //DB.BuildInstructionList((ushort)(x + 512), Memory[(ushort)(x + 512)], Memory[(ushort)((x + 512) +1)]);

                DB.Show();

                Array.Copy(Memory, ResetMemory, 4096); //Save Loaded Game for Reset
                EmuThread = new Thread(new ThreadStart(StartEmulation));
                EmuThread.IsBackground = true;
                EmuThread.Start();
                timer1.Enabled = true;
            }
         }

        private void closeRomToolStripMenuItem_Click(object sender, EventArgs e)
        {
            CloseRom();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            
        }

        private void timer1_Tick(object sender, EventArgs e)
        {
            if (StopEmulation == true) toolStripStatusLabel1.Text = "Emulation Stopped"; else toolStripStatusLabel1.Text = "Emulation Started";
        }


        private void frmEmu_KeyDown(object sender, KeyEventArgs e)
        {
            switch (e.KeyCode)
            {
                case Keys.D0:
                    LastKeyPressed = 0x0;
                    break;
                case Keys.D1:
                    LastKeyPressed = 0x01;
                    break;
                case Keys.D2:
                    LastKeyPressed = 0x02;
                    break;
                case Keys.D3:
                    LastKeyPressed = 0x03;
                    break;
                case Keys.D4:
                    LastKeyPressed = 0x04;
                    break;
                case Keys.D5:
                    LastKeyPressed = 0x05;
                    break;
                case Keys.D6:
                    LastKeyPressed = 0x06;
                    break;
                case Keys.D7:
                    LastKeyPressed = 0x07;
                    break;
                case Keys.D8:
                    LastKeyPressed = 0x08;
                    break;
                case Keys.D9:
                    LastKeyPressed = 0x09;
                    break;
                case Keys.Q:
                    LastKeyPressed = 0x0A;
                    break;
                case Keys.W:
                    LastKeyPressed = 0x0B;
                    break;
                case Keys.E:
                    LastKeyPressed = 0x0C;
                    break;
                case Keys.R:
                    LastKeyPressed = 0x0D;
                    break;
                case Keys.T:
                    LastKeyPressed = 0x0E;
                    break;
                case Keys.Y:
                    LastKeyPressed = 0x0F;
                    break;
                case Keys.P: //Pause
                    if (Control.ModifierKeys == Keys.Control)
                    {
                        PauseEmulation ^= true;
                    }
                    break;
                default:
                    break;
            }
        }

        private void DelayTimer_Tick(object sender, EventArgs e)
        {
            if (Delay_Timer > 0)
                Delay_Timer--;

            if (Sound_Timer > 0)
                Sound_Timer--;
            
            //Display input
            this.KeyPressed.Text = "Last Key Pressed (" + LastKeyPressed.ToString("X") + ")";

            //if (Sound_Timer == 0)
            //    SystemSounds.Beep.Play();
               
        }


        private void pausePToolStripMenuItem_Click(object sender, EventArgs e)
        {
            PauseEmulation ^= true;
        }

        private void resetToolStripMenuItem_Click(object sender, EventArgs e)
        {
            ResetGame();

        }
        void m_ControlContext_Paint(object sender, PaintEventArgs e)
        {
           
            //if (StopEmulation == true)
            //    return;

            if (!m_GContext.IsCurrent)
                Console.WriteLine("Warning context isn't on current thread!");

            GL.Viewport(m_ControlContext.ClientRectangle);

            float m_ScaleX = (float)m_ControlContext.Width / (float)64;
            float m_ScaleY = (float)m_ControlContext.Height / (float)32;

            //Set Clear Color
            GL.ClearColor(Color.FromArgb(Color.Black.ToArgb()));
            GL.Clear(ClearBufferMask.ColorBufferBit);
            GL.MatrixMode(MatrixMode.Modelview);
            GL.LoadIdentity();
            OpenTK.Matrix4 matrix = OpenTK.Matrix4.CreateOrthographicOffCenter(0, (float)m_ControlContext.Width, (float)m_ControlContext.Height, 0, -1, 100);
            GL.LoadMatrix(ref matrix);
            GL.Begin(BeginMode.Quads);
            
            //Set Draw Color
            GL.Color4(Color.FromArgb(Color.Lime.ToArgb()));
         
            //Set new scal from original screen size
            float xscale = ((float)m_ControlContext.Width / (float)64);
            float yscale = ((float)m_ControlContext.Height / (float)32);
            int a = 0;
            int PixelCounter = 0;
            for (int b = 0; b < 32; b++)
            {
                while (a < FrameBuffer.Length)
                {
                    if (PixelCounter < 64)
                    {
                        if (FrameBuffer[a] == 1)
                        {
                            //Draw on Vertex
                            GL.Vertex2(PixelCounter * xscale, b * yscale);
                            GL.Vertex2((PixelCounter * xscale) + xscale, b * yscale);
                            GL.Vertex2((PixelCounter * xscale) + xscale, (b * yscale) + yscale);
                            GL.Vertex2(PixelCounter * xscale, (b * yscale) + yscale);
                        }
                    }

                    if (PixelCounter == 64)
                    {
                        PixelCounter = 0;
                        break;
                    }

                    PixelCounter++;
                    a++;
                }
            }

            GL.End();
            GL.LoadIdentity();

            //Update Screen
            m_GContext.SwapBuffers();
            Thread.Sleep(8);
            m_DrawWait.Set();
        }
        private void frmEmu_Load(object sender, EventArgs e)
        {
            InitOpenTK();
        }

        private void exitToolStripMenuItem_Click(object sender, EventArgs e)
        {
            this.Close();
        }

        private void debuggerToolStripMenuItem_Click(object sender, EventArgs e)
        {
               DB.Show();
        }


 

    }
}
